/*
 * TMap Designer - A Thematic Map design software
 * Copyright (C) 2006 World Health Organization
 * http://www.who.int
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package who.tmap.imp.shapefile.impl;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.apache.commons.io.EndianUtils;

import who.tmap.common.TMapException;
import who.tmap.imp.shapefile.MainHeader;
import who.tmap.imp.shapefile.ShapeFile;
import who.tmap.imp.shapefile.ShapeFileObjects;
import who.tmap.imp.shapefile.shapes.Box;
import who.tmap.imp.shapefile.shapes.MultiPatch;
import who.tmap.imp.shapefile.shapes.MultiPoint;
import who.tmap.imp.shapefile.shapes.MultiPointM;
import who.tmap.imp.shapefile.shapes.MultiPointZ;
import who.tmap.imp.shapefile.shapes.NullShape;
import who.tmap.imp.shapefile.shapes.Point;
import who.tmap.imp.shapefile.shapes.PointM;
import who.tmap.imp.shapefile.shapes.PointZ;
import who.tmap.imp.shapefile.shapes.PolyLine;
import who.tmap.imp.shapefile.shapes.PolyLineM;
import who.tmap.imp.shapefile.shapes.PolyLineZ;
import who.tmap.imp.shapefile.shapes.Polygon;
import who.tmap.imp.shapefile.shapes.PolygonM;
import who.tmap.imp.shapefile.shapes.PolygonZ;
import who.tmap.imp.shapefile.shapes.Range;
import who.tmap.imp.shapefile.shapes.Shape;
import who.tmap.imp.shapefile.shapes.impl.ShapeFactory;

/**
 * @author stl
 *
 * This class reads ArcInfo shape files
 */
public class ShapeFileImport extends BaseShapeFileIO {

    private static Logger logger =Logger.getLogger(ShapeFileImport.class);
    
    private FileInputStream fis;
    private DataInputStream dis; 	
    
    public ShapeFileImport() {
    	super();
    }
    
    /**
     * Reads a little endian int
     * @return little endian int
     * @throws IOException
     */
    private int readIntLittleEndian() throws IOException
    {
    	return EndianUtils.readSwappedInteger( dis );
    }
    
    /**
     * Reads a little endian double
     * @return little endian double
     * @throws IOException
     */
    private double readDoubleLittleEndian() throws IOException
    {
    	return EndianUtils.readSwappedDouble( dis );
    }    
    
    /**
     * Open the shape file
     * @param file
     * @throws FileNotFoundException
     */
    public void openFile( File file ) throws FileNotFoundException {
        this.file =file;
        
        fis = new FileInputStream( file );
        dis = new DataInputStream( fis );
    }
    
    /**
     * Close the shape file
     * @throws IOException
     */
    public void closeFile() throws IOException {
        dis.close();
        fis.close();
    }    
    
    public ShapeFile readShapeFile( File file ) throws IOException, TMapException {
        Exception e2 =null;        
        ShapeFile shapeFile =(ShapeFile)sfoFactory.getShapeFileObject( ShapeFileObjects.SHAPE_FILE );
        try {
	        openFile( file );
	        
	        shapeFile.setFile( file );
	        shapeFile.setMainHeader( readMainFileHeader() );
	        shapeFile.setShapes( readRecords() );
	        
        } catch( Exception e ) {
            e2 =e;
        } finally {
            closeFile();
        }
        
        if ( e2 != null ) {
            throw new TMapException( e2 );
        }
        
        return shapeFile;
        
    }
    
    /**
     * Read Main file header 100 bytes 
     * @throws IOException
     * @throws TMapException
     *
     */
    public MainHeader readMainFileHeader() throws IOException, TMapException {

        MainHeader header =(MainHeader)sfoFactory.getShapeFileObject( ShapeFileObjectFactory.MAIN_HEADER );
        
        // Big endian !
        header.setFileCode( dis.readInt() );
        if ( header.getFileCode() != SHAPE_FILE_CODE ) {
            throw new TMapException( "Invalid shape file code [" + header.getFileCode() + "]" );
        }
        
        // 5 unused integers here
        int unused =dis.readInt();
        unused =dis.readInt();
        unused =dis.readInt();
        unused =dis.readInt();
        unused =dis.readInt();
        
        // Big endian !
        header.setFileLength( dis.readInt() );
        header.setVersion( readIntLittleEndian() );
        header.setShapeType( readIntLittleEndian() );
        header.setXMin( readDoubleLittleEndian() );
        header.setYMin( readDoubleLittleEndian() );
        header.setXMax( readDoubleLittleEndian() );
        header.setYMax( readDoubleLittleEndian() );
        header.setZMin( readDoubleLittleEndian() );
        header.setZMax( readDoubleLittleEndian() );
        header.setMMin( readDoubleLittleEndian() );
        header.setMMax( readDoubleLittleEndian() );

        logger.debug( header );
        
        return header;
        
    }
    
    private Box readBox() throws TMapException, IOException {
        Box box =(Box)shapeFactory.getShape( ShapeFactory.BOX );
        
        box.setXMin( readDoubleLittleEndian() );
        box.setYMin( readDoubleLittleEndian() );
        box.setXMax( readDoubleLittleEndian() );
        box.setYMax( readDoubleLittleEndian() );
        
        return box;
    }
    
    private double[] readMeasures( int nbMeasures ) throws IOException {
	    double[] mArray =new double[ nbMeasures ];
	    for( int ct =0; ct < nbMeasures; ct++ ) {
	        mArray[ ct ] =readDoubleLittleEndian();
	    }	 
	    
	    return mArray;
    }
    
    private Point readPoint() throws TMapException, IOException {
	    Point point =(Point)shapeFactory.getShape( ShapeFactory.POINT );
	    point.setX( readDoubleLittleEndian() );
	    point.setY( readDoubleLittleEndian() );
	    
	    return point;
    }
    
    private PointM readPointM() throws TMapException, IOException {
	    PointM pointM =(PointM)shapeFactory.getShape( ShapeFactory.POINT_M );
	    
	    Point point =readPoint();
	    pointM.setX( point.getX() );
	    pointM.setY( point.getY() );
	    pointM.setM( readDoubleLittleEndian() );
	    
	    return pointM;
    }
    
    private PointZ readPointZ() throws TMapException, IOException {
        PointZ pointZ =(PointZ)shapeFactory.getShape( ShapeFactory.POINT_Z );
	    
	    Point point =readPoint();
	    pointZ.setX( point.getX() );
	    pointZ.setY( point.getY() );
	    pointZ.setZ( readDoubleLittleEndian() );
	    pointZ.setM( readDoubleLittleEndian() );
	    
	    return pointZ;
    }    
    
    
    private Point[] readPoints( int nbPoints ) throws TMapException, IOException {
	    Point[] points =new Point[ nbPoints ];
	    for( int ct =0; ct < nbPoints; ct++ ) {
	        points[ ct ] =readPoint();
	    }
	    
	    return points;
    }
    
    private int[] readParts( int nbParts ) throws TMapException, IOException {
	    int[] parts =new int[ nbParts ];
	    for( int ct =0; ct < nbParts; ct++ ) {
	        parts[ ct ] =readIntLittleEndian();
	    }
	    
	    return parts;
    }
    
    private MultiPoint readMultiPoint() throws TMapException, IOException {
	    MultiPoint multiPoint =(MultiPoint)shapeFactory.getShape( ShapeFactory.MULTI_POINT );
	    multiPoint.setBox( readBox() );
	    
	    multiPoint.setPoints( readPoints( readIntLittleEndian() ) );
	    
	    return multiPoint;
    }
    
    private MultiPointM readMultiPointM() throws TMapException, IOException {
	    MultiPointM multiPointM =(MultiPointM)shapeFactory.getShape( ShapeFactory.MULTI_POINT_M );

	    MultiPoint multiPoint =readMultiPoint();
	    multiPointM.setBox( multiPoint.getBox() );
	    multiPointM.setPoints( multiPoint.getPoints() );
	    
	    multiPointM.setMRange( readRange() );
	    multiPointM.setMArray( readMeasures( multiPointM.getPoints().length ) );	    
	    
	    return multiPointM;
    }  
    
    private MultiPointZ readMultiPointZ() throws TMapException, IOException {
	    MultiPointZ multiPointZ =(MultiPointZ)shapeFactory.getShape( ShapeFactory.MULTI_POINT_Z );
	    multiPointZ.setBox( readBox() );
	    
	    multiPointZ.setPoints( readPoints( readIntLittleEndian() ) );
	    
	    multiPointZ.setZRange( readRange() );
	    multiPointZ.setZArray( readMeasures( multiPointZ.getPoints().length ) );
	    
	    multiPointZ.setMRange( readRange() );
	    multiPointZ.setMArray( readMeasures( multiPointZ.getPoints().length ) );
	    
	    return multiPointZ;
    }        
    
    private PolyLine readPolyLine() throws TMapException, IOException {
        PolyLine polyline =(PolyLine)shapeFactory.getShape( ShapeFactory.POLYLINE );
        polyline.setBox( readBox() );
	    
	    int nbParts =readIntLittleEndian();
	    int nbPoints =readIntLittleEndian();
	    polyline.setParts( readParts( nbParts ) );
	    polyline.setPoints( readPoints( nbPoints ) );
	    
	    return polyline;
    }  
    
    private PolyLineM readPolyLineM() throws TMapException, IOException {
        PolyLineM polylineM =(PolyLineM)shapeFactory.getShape( ShapeFactory.POLYLINE_M );
        PolyLine polyline =readPolyLine();
	    
        polylineM.setBox( polyline.getBox() );
        polylineM.setParts( polyline.getParts() );
        polylineM.setPoints( polyline.getPoints() );
        
        polylineM.setMRange( readRange() );
        polylineM.setMArray( readMeasures( polylineM.getPoints().length ) );
	    
	    return polylineM;
    } 
    
    private PolyLineZ readPolyLineZ() throws TMapException, IOException {
        PolyLineZ polylineZ =(PolyLineZ)shapeFactory.getShape( ShapeFactory.POLYLINE_Z );
        PolyLine polyline =readPolyLine();
	    
        polylineZ.setBox( polyline.getBox() );
        polylineZ.setParts( polyline.getParts() );
        polylineZ.setPoints( polyline.getPoints() );
        
        polylineZ.setZRange( readRange() );
        polylineZ.setZArray( readMeasures( polylineZ.getPoints().length ) );        
        
        polylineZ.setMRange( readRange() );
        polylineZ.setMArray( readMeasures( polylineZ.getPoints().length ) );
	    
	    return polylineZ;
    }        
    
    private Polygon readPolygon() throws TMapException, IOException {
	    Polygon polygon =(Polygon)shapeFactory.getShape( ShapeFactory.POLYGON );
	    polygon.setBox( readBox() );
	    
	    /*logger.debug( "box=[" + polygon.getBox().getXMin() + ", " + polygon.getBox().getYMin() + ", " 
	    					  + polygon.getBox().getXMax() + ", " + polygon.getBox().getYMax() + "]" );*/
	    
	    int nbParts =readIntLittleEndian();
	    int nbPoints =readIntLittleEndian();
	    
	    //logger.debug( "nbParts=[" + nbParts + "], nbPoints=[" + nbPoints + "]" );
	    polygon.setParts( readParts( nbParts ) );
	    polygon.setPoints( readPoints( nbPoints ) );
	    
	    return polygon;
    }
    
    private Range readRange() throws TMapException, IOException {
	    Range range =(Range)shapeFactory.getShape( ShapeFactory.RANGE );
	    range.setMin( readDoubleLittleEndian() );
	    range.setMax( readDoubleLittleEndian() );
	    
	    return range;
    }
    
    private PolygonM readPolygonM() throws TMapException, IOException {
	    PolygonM polygonM =(PolygonM)shapeFactory.getShape( ShapeFactory.POLYGON_M );
	    
	    Polygon polygon =readPolygon();
	    polygonM.setBox( polygon.getBox() );
	    polygonM.setParts( polygon.getParts() );
	    polygonM.setPoints( polygon.getPoints() );
	    
	    polygonM.setMRange( readRange() );
	    polygonM.setMArray( readMeasures( polygonM.getPoints().length ) );    
	    
	    return polygonM;
    }    
    
    private PolygonZ readPolygonZ() throws TMapException, IOException {
        PolygonZ polygonZ =(PolygonZ)shapeFactory.getShape( ShapeFactory.POLYGON_Z );
	    
	    Polygon polygon =readPolygon();
	    polygonZ.setBox( polygon.getBox() );
	    polygonZ.setParts( polygon.getParts() );
	    polygonZ.setPoints( polygon.getPoints() );
	    
	    polygonZ.setZRange( readRange() );
	    polygonZ.setZArray( readMeasures( polygonZ.getPoints().length ) );	    
	    
	    polygonZ.setMRange( readRange() );
	    polygonZ.setMArray( readMeasures( polygonZ.getPoints().length ) );    
	    
	    return polygonZ;
    }    
    
    private MultiPatch readMultiPatch() throws TMapException, IOException {
        MultiPatch multiPatch =(MultiPatch)shapeFactory.getShape( ShapeFactory.MULTI_PATCH );
        
        multiPatch.setBox( readBox() );
	    int nbParts =readIntLittleEndian();
	    int nbPoints =readIntLittleEndian();  
	    
	    multiPatch.setParts( readParts( nbParts ) );
	    // Parts types are coded as integer so we can use readParts
	    // to read parts types too
	    multiPatch.setPartTypes( readParts( nbParts ) );
	    
	    multiPatch.setPoints( readPoints( nbPoints ) );
	    multiPatch.setZRange( readRange() );
	    multiPatch.setZArray( readMeasures( nbPoints ) );	    
	    
	    multiPatch.setMRange( readRange() );
	    multiPatch.setMArray( readMeasures( nbPoints ) ); 	    
        
        return multiPatch;
    }
    
    private Shape readRecord() throws IOException, TMapException {
        Shape shape =null;
        
        if ( dis.available() > 0 ) {
            int recordNumber =dis.readInt();
            int contentLength =dis.readInt();
            int shapeType =readIntLittleEndian();
            
            //logger.debug( "recordNumber=[" + recordNumber + "], contentLength=[" + contentLength + "]" );
            
            switch( shapeType ) {
            	case NULL_SHAPE:
            	    shape =(NullShape)shapeFactory.getShape( ShapeFactory.NULL_SHAPE );
            	    break;
            	    
            	case POINT:
            	    shape =(Shape)readPoint();
            	    break;
            	    
            	case POINT_M:
            	    shape =(Shape)readPointM();
            	    break;
            	    
            	case POINT_Z:
            	    shape =(Shape)readPointZ();
            	    break;
            	    
            	case MULTI_POINT:
            	    shape =(Shape)readMultiPoint();
            	    break;
            	    
            	case MULTI_POINT_M:
            	    shape =(Shape)readMultiPointM();
            	    break;    
            	    
            	case MULTI_POINT_Z:
            	    shape =(Shape)readMultiPointZ();
            	    break;               	    
            	    
            	case POLYLINE:
            	    shape =(Shape)readPolyLine();
            	    break;
            	    
            	case POLYLINE_M:
            	    shape =(Shape)readPolyLineM();
            	    break;     
            	    
            	case POLYLINE_Z:
            	    shape =(Shape)readPolyLineZ();
            	    break;               	    
            	    
            	case POLYGON:
            	    shape =(Shape)readPolygon();
            	    break;

            	case POLYGON_M:
            	    shape =(Shape)readPolygonM();
            	    break;        
            	    
            	case POLYGON_Z:
            	    shape =(Shape)readPolygonZ();
            	    break;  
            	    
            	case MULTI_PATCH:
            	    shape =(Shape)readMultiPatch();
            	    break;              	    
            	
            	default:
            	    throw new TMapException( "Invalid shape type [" + shapeType + "]" );
            }
        }
        
        return shape;
    }
    
    public Shape[] readRecords() throws IOException, TMapException {
        
        List<Shape> records =new ArrayList<Shape>();
        
        while( dis.available() > 0 ) {
            Shape shape =readRecord();
            
            records.add( shape );
        }
        
        logger.debug( "Nb of shapes=[" + records.size() + "]" ); 
        
        Shape[] recordsArr =new Shape[ records.size() ];
        return (Shape[])records.toArray( recordsArr );
    }
}
